package se.kayarr.cardplay.gameplay;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lombok.Getter;

import org.mozilla.javascript.Context;

import se.kayarr.cardplay.exceptions.PositionOutsideFieldException;
import se.kayarr.cardplay.exceptions.UnsetVariableException;
import se.kayarr.cardplay.shared.ClampedValue;

public class UnitCard extends Card {
	public static final String TYPE_SYMBOL = "U";
	
	@Getter private int maxHealth = -1;
	@Getter private Type type = Type.NEUTRAL;
	
	private List<Ability> abilities = new ArrayList<>();
	private List<Ability> cachedImmutableList;
	
	public List<Ability> getAbilities() {
		if(cachedImmutableList == null) cachedImmutableList = Collections.unmodifiableList(abilities);
		return cachedImmutableList; //Just returns an immutable list, as we don't need to deal with
														//using an exclusion set
	}
	
	public String getTypeSymbol() {
		return TYPE_SYMBOL;
	}
	
	public UnitCard(int numericId) {
		super(numericId);
	}
	
	public void setMaxHealth(int maxHealth) {
		checkMutable();
		this.maxHealth = maxHealth;
	}
	
	public void setType(Type type) {
		checkMutable();
		if(type == null) this.type = Type.NEUTRAL;
		else this.type = type;
	}
	
	@Override
	protected void checkRequiredVarsSet() throws UnsetVariableException {
		super.checkRequiredVarsSet();
		
		if(maxHealth <= 0) throw new UnsetVariableException("maxHealth");
	}
	
	public Ability addAbility(String name, String desc, int targetFlags, Ability.OnUseCallback onUseFunc) {
		checkMutable();
		Ability ability = new Ability(name, desc, targetFlags, onUseFunc);
		abilities.add(ability);
		return ability;
	}
	
	public Ability addAbility(Ability ability) {
		checkMutable();
		abilities.add(ability);
		return ability;
	}
	
	public void removeAbility(Ability ability) {
		checkMutable();
		abilities.remove(ability);
	}
	
	public static class Ability {
		@Getter String name;
		@Getter String desc;
		@Getter OnUseCallback onUseFunc;
		
		@Getter private int targetFlags = Targetable.NONE;
		
		public boolean allTargetFlagsSet(int targetFlags) {
			return ((this.targetFlags & targetFlags) == targetFlags);
		}
		
		public boolean anyTargetFlagsSet(int targetFlags) {
			return ((this.targetFlags & targetFlags) != 0);
		}
		
		private Ability(String name, String desc, int targetFlags,
				OnUseCallback onUseFunc) {
			this.name = name;
			this.desc = desc;
			this.targetFlags = targetFlags;
			this.onUseFunc = onUseFunc;
		}
		
		public boolean onUse(Session session, Player player, UnitCard.Instance unit, Targetable target) {
			if(onUseFunc == null) {
				session.narrate("You can't use this ability; it doesn't do anything!");
				return false;
			}
			
			return onUseFunc.onUse(this, unit, player, target);
		}
		
		public String toString() {
			StringBuilder builder = new StringBuilder().append("Ability [").append('"').append(name).append('"');
			if(desc.length() > 0) builder.append(": ").append(desc);
			return builder.append(']').toString();
		}
		
		private static interface OnUseCallback {
			public boolean onUse(Ability ability, UnitCard.Instance unit, Player player, Targetable target);
		}
	}
	
	public Instance newInstance(Session session, Player owner) {
		return new Instance(session, owner);
	}

	public class Instance extends Card.Instance implements Targetable {
		@Getter private ClampedValue health = new ClampedValue(maxHealth);
		@Getter private Type type = UnitCard.this.type;
		
		public void setType(Type type) {
			if(type == null) this.type = UnitCard.this.type;
			else this.type = type;
		}
		
		private List<Ability> extraAbilities = new ArrayList<>();
		
		private Set<Ability> excludedAbilities = new HashSet<>();
		
		@Getter int x = -1;
		@Getter int y = -1;
		
		public Ability addAbility(String name, String desc, int targetFlags, Ability.OnUseCallback onUseFunc) {
			Ability ability = new Ability(name, desc, targetFlags, onUseFunc);
			extraAbilities.add(ability);
			return ability;
		}
		
		public Ability addAbility(Ability ability) {
			if(excludedAbilities.contains(ability)) //If ability is present in exclusion set
				excludedAbilities.remove(ability); //Just remove it from the set, making it appear again
			else
				extraAbilities.add(ability); //Otherwise add it to the extra abilities list
			return ability;
		}
		
		public void removeAbility(Ability ability) {
			if(extraAbilities.contains(ability)) //If the extra abilities of this unit has the passed ability
				extraAbilities.remove(ability); //Just remove it, man
			else
				excludedAbilities.add(ability); //Otherwise it's an ability the card defined, so add to exclusion set
												//Making it magically disappear for this particular unit
												//(Doesn't matter if the card doesn't actually have it, really)
		}
		
		public List<Ability> getAbilities() {
			List<Ability> returnList = new ArrayList<>();
			for(Ability ability : abilities) {
				if(!excludedAbilities.contains(ability)) returnList.add(ability);
			}
			returnList.addAll(extraAbilities);
			return Collections.unmodifiableList(returnList);
		}
		
		public List<Ability> getExtraAbilities() {
			return Collections.unmodifiableList(extraAbilities);
		}
		
		public UnitCard getCard() {
			return UnitCard.this;
		}
		
		protected Instance(Session session, Player owner) {
			super(session, owner);
			health.setToMax();
			
			onInit();
		}
		
		public int dealDamage(int damage) {
			return dealDamage(damage, Type.NEUTRAL);
		}
		
		public int dealDamage(int damage, Type damageType) {
			damage *= Type.getDamageMultiplier(damageType, type);
			health.removeAmount(damage);
			return damage;
		}
		
		boolean canPlaceAt(Field field, int x, int y) throws PositionOutsideFieldException {
			if(x < 0 || x >= field.getWidth() || y < 0 || y >= field.getHeight()) throw new PositionOutsideFieldException(x, y);
			
			Object returnval = null;
			if(hasFunction("canPlaceAt") &&
					(returnval = invokeFunction("canPlaceAt", this, field, x, y)) != null) {
				return Context.toBoolean(returnval);
			}
			
			return field.getUnitCard(x, y) == null;
		}
		
		void onPlaced() {
			invokeFunction("onPlaced", this);
		}
		
		void onRoundStart() {
			invokeFunction("onRoundStart", this);
		}
		
		void onRoundEnd() {
			invokeFunction("onRoundEnd", this);
		}
		
		public String toString() {
			if(x < 0 && y < 0) return getDisplayName();
			else return new StringBuilder().append('[').append(getDisplayId()).append(" \"").append(getName()).append("\" ")
					.append(health.toString()).append(" @ ")
					.append(x+1).append(',').append(y+1).append(']').toString();
		}

		@Override
		public String getName() {
			return getCard().getName();
		}
	}
}
